<?php
/**
 * Zend Image Manipulation Module
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.

 * @category   Zimm
 * @package    Zimm
 * @copyright  Copyright (c) 2012 Kristoffer Ingemansson
 * @license    New BSD License
 * @version    $Id: Abstract.php 4 2012-04-14 12:59:16Z kristoffer.ingemansson@gmail.com $
 */

abstract class Zimm_Model_DbTable_Instruction_Abstract extends Zend_Db_Table_Row_Abstract
{
    /**
     * Array containing sub-instructions.
     *
     * @var array
     */
    protected $_instructions = array();

    /**
     * Initialize object.
     *
     * @return void
     */
    public function init()
    {
        if (isset($this->_data['_instructions'])) {
            $className = $this->_table->getRowClass();
            foreach ($this->_data['_instructions'] as $instruction) {
                if ($x = $instruction['instructionClassName']) {
                    if (!strpos($x, '_')) {
                        $x = 'Zimm_Model_DbTable_Instruction_' . $x;
                    }
                    $className = $x;
                }

                $data = array(
                    'table'=>$this->_table,
                    'data'=>$instruction,
                    'readOnly'=>false,
                    'stored'=>true,
                );
                $this->_instructions[] = new $className($data);
            }
        }
    }

    /**
     * Processes an image object and returns its file system path.
     *
     * @param Zend_Db_Table_Row $image Image object
     * @param array $retImageInfo indicates processed image data
     * @return string path to local processed imagefile
     */
    public function process($image, &$retImageInfo = array())
    {
        $file = $image->imageHref;
        $options = Zend_Controller_Front::getInstance()->getParam('bootstrap')->getOption('zimm');

        $imageCachePathPart = rtrim($options['cachepath'], '/') . '/' . $image->imageId . '_';
        $cachePath = $imageCachePathPart . $this->instructionId;

        $retImageInfo = array(
            'last-modified'=>0,
            'etag'=>NULL,
            'content-type'=>$this->_getContentType(),
            'content-length'=>0,
        );

        $mtime = 0;
        $instructionLastModification = $this->_getLastModified();

        if ($readable = is_readable($cachePath)) {
            $mtime = @filemtime($cachePath);
        }

        if (!$readable || $instructionLastModification > date('Y-m-d H:i:s', $mtime)) {
            try {
                if (!$data = $this->_loadFile($file, $responseHeaders)) {
                    foreach (glob($imageCachePathPart . "*", GLOB_NOSORT) as $file) {
                        @unlink($file);
                    }
                    return false;
                }

                $image = imagecreatefromstring($data);
                $this->_processGdImage($image, $cachePath, array());
            } catch (Exception $e) {
                return false;
            }
            if (!is_readable($cachePath)) {
                return false;
            }
        }

        $retImageInfo['content-length'] = @filesize($cachePath);
        $retImageInfo['last-modified'] = $mtime;
        $retImageInfo['etag'] = "ce-".dechex(crc32($cachePath.$retImageInfo['last-modified']));;
        return $cachePath;
    }

    /**
     * Do actual instruction processing
     *
     * @param resource $image GD resource
     * @param string $savePath Target save path
     * @param array $params Instruction arguments
     * @return boolean Indicates if image was altered
     */
    abstract protected function _process(&$image, $savePath, $params);

    /**
     * Loads an local or remote image file
     *
     * @param string $file
     * @param array $responseHeaders
     * @return string
     */
    protected  function _loadFile($file, &$responseHeaders = NULL)
    {
        $data = @file_get_contents($file);
        $responseHeaders = $this->_parseHeaders($http_response_header);
        return $data;
    }

    /**
     * Recursively process image.
     *
     * @param resource $image
     * @param string $savePath Target save path
     * @param array $parentParams
     * @return integer
     */
    protected function _processGdImage(&$image, $savePath, $parentParams = array())
    {
        $ret = 0;
        if ($this->instructionProcessOrder == 'parentfirst') {
            $ret += $this->_process($image, $savePath, array_merge($parentParams, $this->instructionParams));
        }

        foreach ($this->_instructions as $instruction) {
            $ret += $instruction->_processGdImage($image, $savePath, $this->instructionParams);
        }

        if ($this->instructionProcessOrder != 'parentfirst') {
            $ret += $this->_process($image, $savePath, array_merge($parentParams, $this->instructionParams));
        }
        return $ret;
    }

    /**
     * Gets file extension from instruction result content-type.
     *
     * @return string
     */
    public function getExtension()
    {
        $ctype = $this->_getContentType();
        return substr(strrchr($ctype, '/'), 1);
    }

    /**
     * Recursively gets the last save-instructions resulting content-type.
     *
     * @return string Content-type
     */
    protected function _getContentType()
    {
        $contentType = NULL;
        if ($this->instructionProcessOrder == 'parentfirst') {
            $contentType = $this->instructionOutputContentType;
            foreach ($this->_instructions as $instruction) {
                if ($ct = $instruction->_getContentType()) {
                    $contentType = $ct;
                }
            }
        } else {
            if (!$contentType = $this->instructionOutputContentType) {
                foreach ($this->_instructions as $instruction) {
                    if ($ct = $instruction->_getContentType()) {
                        $contentType = $ct;
                    }
                }
            }
        }
        return $contentType;
    }

    /**
     * Get most recent modification Datetime of instruction or subinstruction.
     *
     * @return string MySQL DATETIME
     */
    protected function _getLastModified()
    {
        $last = max($this->dataCreatedDate, $this->dataModifiedDate);
        foreach ($this->_instructions as $instruction) {
            $last = max($last, $instruction->_getLastModified());
        }
        return $last;
    }

    /**
     * Parse response headers for relevant headers
     *
     * @param  array $headers response headers
     * @return mixed Array with extracted headers, or NULL if
     *         no headers found, or FALSE if not 200 or 304
     */
    protected function _parseHeaders($headers) {
        $etag = NULL;
        $lastModified = NULL;
        $contentType = NULL;
        foreach (array_reverse($headers) as $header) {
            if (!$etag && stripos($header, 'etag') !== false) {
                $etag = preg_replace('/^\s*etag\s*:\s*/i', '', $header);
                continue;
            }

            if (!$lastModified && stripos($header, 'last-modified') !== false) {
                $lastModified = preg_replace('/^\s*last-modified\s*:\s*/i', '', $header);
                continue;
            }

            if (!$contentType && stripos($header, 'content-type') !== false) {
                $contentType = preg_replace('/^\s*content-type\s*:\s*/i', '', $header);
                continue;
            }

            if (stripos($header, 'HTTP/1') !== false) {
                if (strpos($header, '200') || strpos($header, '304')) {
                    break;
                }
                return false;
            }
        }

        if ($etag || $lastModified || $contentType) {
            return array('content-type'=>$contentType, 'etag'=>$etag, 'last-modified'=>$lastModified);
        }
        return NULL;
    }

    /**
     * Retrieve row field value.
     * Unserializes instructionParams-column.
     *
     * @param  string $columnName The user-specified column name.
     * @return string             The corresponding column value.
     * @throws Zend_Db_Table_Row_Exception if the $columnName is not a column in the row.
     */
    public function __get($columnName)
    {
        $data = parent::__get($columnName);
        if ($columnName == 'instructionParams') {
            if (!$data) {
                $data = array();
            } else {
                $data = (array)unserialize($data);
            }
        }
        return $data;
    }

    /**
     * Set row field value.
     * Serializes instructionParams-column.
     *
     * @param  string $columnName The column key.
     * @param  mixed  $value      The value for the property.
     * @return void
     * @throws Zend_Db_Table_Row_Exception
     */
    public function __set($columnName, $value)
    {
        if ($columnName == 'instructionParams') {
            $value = serialize((array)$value);
        }
        parent::__set($columnName, $value);
    }

}